using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Oops.Xna.Framework.GameState;
using Oops.Xna.Framework.Graphics;
using Oops.Xna.Framework.Physics;
using Oops.Xna.Framework.Physics.CollisionSystems;
using Oops.Xna.Framework.UI;
using Oops.Xna.Framework.Viewpoint;
using Oops.Xna.Framework.Viewpoint.Cameras;

namespace Oops.Xna.Framework.Testing.App
{
	public abstract class ExampleScreen : GameScreen, ICameraCallback
	{
		// Game components.
		protected ViewpointComponent viewpoint;
		protected PhysicsComponent physics;
		private UIComponent uiComponent;

		// Audio resources.
		private WaveBank waveBank;
		private SoundBank soundBank;

		// Content.
		private Texture2D sphereTexture;
		private Texture2D boxTexture;
		private SpriteFont defaultFont;

		// Drawing resources.
		protected BasicEffect basicEffect;
		private PssmEffect pssmEffect;
		private SpriteBatch spriteBatch;

		// Drawing helpers.
		private VertexDeclaration vertexDeclaration;
		private Sphere sphere = new Sphere(8, 8);
		private Box box = new Box();

		protected bool shadow = false;
		private bool showHelp = false;

		// Collision shapes.  The physics component knows nothing of these collision shapes.  It only operates on the associated rigid bodies
		// and collisions produced.  Re-dimension arrays to add more/less shapes.
		protected List<CollisionSphere> spheres = new List<CollisionSphere>();
		protected List<CollisionBox> boxes = new List<CollisionBox>();

		public ExampleScreen(Game game, string name)
			: base(game, name)
		{
			// Create screen-specific components.
			this.viewpoint = new ViewpointComponent(this.Game);
			this.physics = new PhysicsComponent(this.Game, new SweepAndPruneCollisionSystem());
			//this.physics = new PhysicsComponent(this.Game, new GridCollisionSystem(20.0f));
			this.uiComponent = new UIComponent(this.Game);

			// Add the components to the screen's game component collection.
			this.Components.Add(this.viewpoint);
			this.Components.Add(this.physics);
			this.Components.Add(this.uiComponent);
		}

		#region GameScreen Members

		public override void Enter(GameTime gameTime, ref GameScreen previousScreen)
		{
			// There should be no previous screen processing.
			previousScreen = null;

			// Enable updating when this screen is entered.
			this.Enabled = true;

			// Call inherited method.
			base.Enter(gameTime, ref previousScreen);
		}

		public override bool GetTransition(GameTime gameTime, ref GameScreen newScreen)
		{
			// Display the prompt screen anytime this screen is disabled.
			if (!this.Enabled && this.GameStateService.CurrentScreen == this)
			{
				// Get the prompt screen.
				PromptScreen promptScreen = this.GameStateService.Screens["Prompt"] as PromptScreen;

				promptScreen.Initialize(
					"Are you sure that you want to quit?",
					new PromptCallback(this.OnPromptExit),
					"Yes", "No");

				// Display the prompt screen.
				newScreen = promptScreen;
			}

			// The screen needs updating.
			return true;
		}

		#endregion

		#region GameComponent Members

		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				lock (this)
				{
					// Dispose of audio resources.
					if (this.soundBank != null)
					{
						this.soundBank.Dispose();
					}
					if (this.waveBank != null)
					{
						this.waveBank.Dispose();
					}
				}
			}

			// Call inherited method.
			base.Dispose(disposing);
		}

		#endregion

		#region DrawableGameComponent Members

		protected override void LoadContent()
		{
			// Load all the used texture resources.
			this.sphereTexture = this.Game.GetContent().Load<Texture2D>(@".\Content\Textures\sphere");
			this.boxTexture = this.Game.GetContent().Load<Texture2D>(@".\Content\Textures\box");

			// Create and initialize a basic effect for drawing.
			this.basicEffect = this.Game.GetService<BasicEffect>();

			// Create the pssm effect.
			this.pssmEffect = this.Game.GetService<PssmEffect>();

			// Create a sprite batch and font for text drawing.
			this.spriteBatch = new SpriteBatch(this.GraphicsDevice);
			this.defaultFont = this.Game.GetContent().Load<SpriteFont>(@".\Content\Fonts\default");

			// Create the vertect declaration for primitive drawing.
			this.vertexDeclaration = new VertexDeclaration(this.GraphicsDevice, VertexPositionNormalTexture.VertexElements);

			// Call inherited method.
			base.LoadContent();
		}

		protected override void UnloadContent()
		{
			if (this.vertexDeclaration != null)
			{
				this.vertexDeclaration.Dispose();
			}

			// Dispose of the sprite batch.
			if (this.spriteBatch != null)
			{
				this.spriteBatch.Dispose();
			}

			// Call inherited method.
			base.UnloadContent();
		}

		#endregion

		#region IGameComponent Members

		public override void Initialize()
		{
			// Initialize the viewpoint component.  Add as many cameras as necessary.  The last camera is the starting camera.
			// Switch cameras at anytime and the viewpoint will change automatically.
			FreeLookCamera freeLookCamera = new FreeLookCamera(this.Game, "Free Look Camera");
			freeLookCamera.Position = new Vector3(0.0f, 120.0f, 120.0f);
			freeLookCamera.Orientation = Matrix.CreateRotationX(-MathHelper.PiOver4);
			this.viewpoint.Cameras.Add(freeLookCamera);

			ArcBallCamera arcBallCamera = new ArcBallCamera(this.Game, "Arc Ball Camera");
			arcBallCamera.Target = Vector3.Up * 5.0f;
			arcBallCamera.Position = new Vector3(0.0f, 120.0f, 120.0f);
			this.viewpoint.Cameras.Add(arcBallCamera);

			LookAtCamera lookAtCamera = new LookAtCamera(this.Game, "Look At Camera", this);
			lookAtCamera.Position = new Vector3(0.0f, 120.0f, 120.0f);
			this.viewpoint.Cameras.Add(lookAtCamera);

			ChaseCamera chaseCamera = new ChaseCamera(this.Game, "Chase Camera", this);
			this.viewpoint.Cameras.Add(chaseCamera);

			StaticCamera staticCamera = new StaticCamera(this.Game, "Static Camera");
			staticCamera.Position = new Vector3(0.0f, 120.0f, 120.0f);
			staticCamera.Orientation = Matrix.CreateRotationX(-MathHelper.PiOver4);
			this.viewpoint.Cameras.Add(staticCamera);

			// Call inherited method.
			base.Initialize();

			// Reset all collision shapes to initialize them.
			this.Reset();

			// Initialize the ui component.  Just display the instructions.
			Control label = new Control(this.Game, "DirectionsLabel");
			label.Location = new Point(this.GraphicsDevice.Viewport.TitleSafeArea.Right - 408, this.GraphicsDevice.Viewport.TitleSafeArea.Top + 8);
			label.Size = new Size(400, 280);
			label.TextAlign = ContentAlignment.TopLeft;
			label.Text =
				"<R>           or <A Button>       Reset\r\n" +
				"<Space>  or <Y Button>       Toggle Shadows\r\n" +
				"<Tab>       or <DPad-Up>       Camera Toggle\r\n" +
				"<F1>         or <DPad-Left>     Wireframe Toggle\r\n" +
				"<F2>         or <DPad-Right>   Draw AABB Toggle\r\n" +
				"<F3>         or <DPad-Down>  No Draw Toggle\r\n" +
				"\r\n" +
				"<Esc>       or <Back Btn>       Exit";
			this.uiComponent.Controls.Add(label);

			// Change the input service's settings.
			this.InputService.ThumbStickDeadZone = GamePadDeadZone.Circular;

			// Create the wave bank and sound bank.
			this.waveBank = new WaveBank(this.AudioService.AudioEngine, @".\Content\Audio\Examples.xwb");
			this.soundBank = new SoundBank(this.AudioService.AudioEngine, @".\Content\Audio\Examples.xsb");
		}

		#endregion

		#region IUpdateable Members

		private const float ForceFactor = 38000.0f;

		public override void Update(GameTime gameTime)
		{
			// Wait for the last frame's physics simulation to finish.
			this.physics.EndUpdate();

			for (int i = 0; i < 4; i++)
			{
				// Gamepad controls.
				if (this.InputService.IsGamePadConnected(i))
				{
					// The first gamepad controls has special options.
					if (i == 0)
					{
						if (this.InputService.IsGamePadButtonPressed(i, Buttons.Back)) this.Enabled = false;
						if (this.InputService.IsGamePadButtonPressed(i, Buttons.A)) this.Reset();
						if (this.InputService.IsGamePadButtonPressed(i, Buttons.Y)) this.shadow = !this.shadow;

						this.showHelp = this.InputService.IsGamePadButtonDown(i, Buttons.Start);
					}

					// Move associated sphere based on input.
					Vector2 thumbStick = this.InputService.GetGamePadLeftThumbStick(i);
					Vector3 torque = new Vector3(
						thumbStick.Y * -ForceFactor,
						(this.InputService.GetGamePadLeftTrigger(i) - this.InputService.GetGamePadRightTrigger(i)) * ForceFactor,
						thumbStick.X * -ForceFactor);

					// Only moving the the sphere by adding torque/spin.
					if (i < this.spheres.Count)
					{
						this.spheres[i].CollisionGroup.RigidBody.ApplyWorldTorque(torque);
					}
				}

				// Keyboard controls.
				if (i == 0)
				{
					if (this.InputService.IsKeyPressed(Keys.Escape)) this.Enabled = false;
					if (this.InputService.IsKeyPressed(Keys.R)) this.Reset();
					if (this.InputService.IsKeyPressed(Keys.Space)) this.shadow = !this.shadow;

					this.showHelp = this.InputService.IsKeyDown(Keys.H);

					if (this.InputService.IsKeyPressed(Keys.PageUp)) this.pssmEffect.SplitCount++;
					if (this.InputService.IsKeyPressed(Keys.PageDown)) this.pssmEffect.SplitCount = Math.Max(0, this.pssmEffect.SplitCount - 1);

					// Move associated sphere based on input.
					Vector3 torque = Vector3.Zero;
					if (this.InputService.IsKeyDown(Keys.LeftControl)) torque.Y += ForceFactor;
					if (this.InputService.IsKeyDown(Keys.LeftAlt)) torque.Y -= ForceFactor;
					if (this.InputService.IsKeyDown(Keys.Up)) torque.X -= ForceFactor;
					if (this.InputService.IsKeyDown(Keys.Left)) torque.Z += ForceFactor;
					if (this.InputService.IsKeyDown(Keys.Down)) torque.X += ForceFactor;
					if (this.InputService.IsKeyDown(Keys.Right)) torque.Z -= ForceFactor;

					// Only moving the the sphere by adding torque/spin.
					if (i < this.spheres.Count)
					{
						this.spheres[i].CollisionGroup.RigidBody.ApplyWorldTorque(torque);
					}
				}
			}

			// Start this frame's physics simulation.
			this.physics.BeginUpdate(gameTime);

			this.uiComponent.Enabled = this.showHelp;
			this.uiComponent.Visible = this.showHelp;

			// Call inherited method.
			base.Update(gameTime);
		}

		#endregion

		#region IDrawable Members

		public override void Draw(GameTime gameTime)
		{
			if (this.shadow)
			{
				this.pssmEffect.Camera = this.viewpoint.ActiveCamera;

				// Start the parallel split shadow map process.
				this.pssmEffect.BeginPssm(new BoundingBox(new Vector3(-200.0f), new Vector3(200.0f)));

				// Iterate on the parallel splits.
				for (int i = 0; i < this.pssmEffect.SplitCount; i++)
				{
					// Create the shadow map.
					this.pssmEffect.BeginCreateShadow(i);
					this.DrawScene(this.pssmEffect, this.pssmEffect.Parameters["World"], this.pssmEffect.Parameters["DiffuseTexture"]);
					this.pssmEffect.EndCreateShadow(i);
				}

				// Draw the shadowed scene.
				this.pssmEffect.BeginApplyShadow();

				// Clear the graphics device and assign the vertex declaration.
				this.GraphicsDevice.Clear(ClearOptions.DepthBuffer | ClearOptions.Target, Color.Black, 1.0f, 0);

				this.DrawScene(this.pssmEffect, this.pssmEffect.Parameters["World"], this.pssmEffect.Parameters["DiffuseTexture"]);

				this.pssmEffect.EndApplyShadow();

				this.pssmEffect.EndPssm();
			}
			else
			{
				// Clear the graphics device and assign the vertex declaration.
				this.GraphicsDevice.Clear(ClearOptions.DepthBuffer | ClearOptions.Target, Color.Black, 1.0f, 0);

				// Set the view and projection effect parameters.
				this.basicEffect.View = this.viewpoint.ActiveCamera.View;
				this.basicEffect.Projection = this.viewpoint.ActiveCamera.Projection;

				this.GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
				this.GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;

				// Draw the scene.
				this.DrawScene(this.basicEffect, this.basicEffect.Parameters["World"], this.basicEffect.Parameters["BasicTexture"]);
			}

			this.spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);

			// Draw camera name.
			Vector2 cameraNameSize = this.defaultFont.MeasureString(this.viewpoint.ActiveCamera.Name);
			Vector2 position = new Vector2()
			{
				X = (this.GraphicsDevice.Viewport.Width * 0.5f) - (cameraNameSize.X * 0.5f),
				Y = this.GraphicsDevice.Viewport.TitleSafeArea.Bottom - cameraNameSize.Y - 10.0f
			};
			this.spriteBatch.DrawString(this.defaultFont, this.viewpoint.ActiveCamera.Name, position, Color.White);

			// Draw help.
			if (!this.showHelp)
			{
				Vector2 size = this.defaultFont.MeasureString("Press \"H\" for help");
				position.X = (this.GraphicsDevice.Viewport.Width * 0.5f) - (size.X * 0.5f);
				position.Y -= cameraNameSize.Y + 10.0f;
				this.spriteBatch.DrawString(this.defaultFont, "Press \"H\" for help", position, Color.White);
			}

			this.spriteBatch.End();

			// Call inherited method.
			base.Draw(gameTime);
		}

		#endregion

		public virtual void DrawScene(Effect effect, EffectParameter world, EffectParameter texture)
		{
			// Initialize the basic effect for this iteration.
			effect.Begin();
			effect.CurrentTechnique.Passes[0].Begin();

			// Draw the scene.
			this.GraphicsDevice.VertexDeclaration = this.vertexDeclaration;
			this.DrawWorld(effect, world, texture);

			// Draw all the box shapes.
			texture.SetValue(this.boxTexture);
			for (int j = 0; j < this.boxes.Count; j++)
			{
#if QUATERNION_ORIENTATION
				world.SetValue(Matrix.CreateScale(this.boxes[j].Extents) * Matrix.CreateFromQuaternion(this.boxes[j].Orientation) * Matrix.CreateTranslation(this.boxes[j].Position));
#else
				world.SetValue(Matrix.CreateScale(this.boxes[j].Extents) * this.boxes[j].Orientation * Matrix.CreateTranslation(this.boxes[j].Position));
#endif
				this.DrawShape(effect, this.boxes[j]);
				effect.CommitChanges();
				this.GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, this.box.Vertices, 0, this.box.Vertices.Length / 3);
			}

			// Draw all the sphere shapes.
			texture.SetValue(this.sphereTexture);
			for (int j = 0; j < this.spheres.Count; j++)
			{
				// NOTE : Access to the sphere's position and orientation are not thread-safe!!!  I'm doing so here because I'm lazy.
#if QUATERNION_ORIENTATION
				world.SetValue(Matrix.CreateScale(this.spheres[j].Radius) * Matrix.CreateFromQuaternion(this.spheres[j].Orientation) * Matrix.CreateTranslation(this.spheres[j].Position));
#else
				world.SetValue(Matrix.CreateScale(this.spheres[j].Radius) * this.spheres[j].Orientation * Matrix.CreateTranslation(this.spheres[j].Position));
#endif
				this.DrawShape(effect, this.spheres[j]);
				effect.CommitChanges();
				this.GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, this.sphere.Vertices, 0, this.sphere.Vertices.Length, this.sphere.Indices, 0, this.sphere.Indices.Length / 3);
			}

			// End the basic effect processing.
			effect.CurrentTechnique.Passes[0].End();
			effect.End();
		}

		protected virtual void DrawShape(Effect effect, CollisionShape collisionShape)
		{
		}

		protected abstract void DrawWorld(Effect effect, EffectParameter world, EffectParameter texture);

		#region Prompt Handlers

		private bool OnPromptExit(int selectedOption, GameTime gameTime, ref GameScreen newScreen)
		{
			// Check which option was selected.
			switch (selectedOption)
			{
				case -1:
					// Back to game.
					newScreen = this;
					break;

				case 0:
					// Back to main menu.
					newScreen = this.GameStateService.Screens["MainMenu"];
					break;

				case 1:
					// Back to game.
					newScreen = this;
					break;
			}

			// The screen needs updating.
			return true;
		}

		#endregion

		protected abstract void Reset();
		protected void Reset(int boxCount, int sphereCount)
		{
			// Reset the quad-tree and shape lists.
			this.boxes.Clear();
			this.spheres.Clear();

			// Clear the physics service of rigid bodies and static collision shapes.
			this.physics.RigidBodies.Clear();
			this.physics.CollisionSystem.CollisionGroups.Clear();

			// Seed the randomizer.
			Random random = new Random(DateTime.Now.Millisecond);

			// Iterate on all box shapes.
			for (int i = 0; i < boxCount; i++)
			{
				// Create the collision box.
				CollisionBox box = new CollisionBox();
				box.Extents = random.NextVector3(2.0f, 7.0f);
				box.StaticFriction = 0.5f;
				box.DynamicFriction = 0.5f;
				box.Restitution = 0.6f;

				// Create the rigid body for the collision box.
				RigidBody rigidBody = new RigidBody();

				// Add collision sphere to the rigid body.
				rigidBody.CollisionGroup.CollisionShapes.Add(box);
				rigidBody.CalculateMassProperties(1.0f);

				// Determine the position and orientation of the box.
				Vector3 position = random.NextVector3(-90.0f, 90.0f, 50.0f, 170.0f, -90.0f, 90.0f);
#if QUATERNION_ORIENTATION
				Quaternion orientation = Quaternion.Normalize(random.NextQuaternion(-90.0f, 90.0f, 50.0f, 70.0f, -90.0f, 90.0f));
#else
				Matrix orientation = Matrix.CreateFromQuaternion(Quaternion.Normalize(random.NextQuaternion(-90.0f, 90.0f, 50.0f, 70.0f, -90.0f, 90.0f)));
#endif
				rigidBody.UpdateTransform(ref position, ref orientation);

				rigidBody.Rotation = random.NextVector3(-0.5f, 0.5f);

				// Assign the box to the quad-tree and shape list.
				this.boxes.Add(box);
				this.physics.RigidBodies.Add(rigidBody);
			}

			// Iterate on all sphere shapes.
			for (int i = 0; i < sphereCount; i++)
			{
				// Create the collision sphere.
				CollisionSphere sphere = new CollisionSphere();
				sphere.Radius = i < 4 ? 7.0f : (float)(random.NextDouble() * 3.0D) + 1.0f;
				sphere.StaticFriction = i < 4 ? 1.0f : 0.7f;
				sphere.DynamicFriction = i < 4 ? 1.0f : 0.7f;
				sphere.Restitution = 0.8f;

				// Create a rigid body for the collision sphere.
				RigidBody rigidBody = new RigidBody();
				rigidBody.Flags = i < 4 ? RigidBodyFlags.None : RigidBodyFlags.AutoDisable;
				rigidBody.AngularDamping = 0.3f;

				// Add collision sphere to the rigid body.
				rigidBody.CollisionGroup.CollisionShapes.Add(sphere);
				rigidBody.CalculateMassProperties(1.0f);

				// Determine the position and orientation of the sphere.
				Vector3 position = i < 4 ? random.NextVector3(-90.0f, 90.0f, 10.0f, 10.0f, -90.0f, 90.0f) : random.NextVector3(-90.0f, 90.0f, 50.0f, 170.0f, -90.0f, 90.0f);
#if QUATERNION_ORIENTATION
				Quaternion orientation = Quaternion.Normalize(random.NextQuaternion(-90.0f, 90.0f, 50.0f, 70.0f, -90.0f, 90.0f));
#else
				Matrix orientation = Matrix.CreateFromQuaternion(Quaternion.Normalize(random.NextQuaternion(-90.0f, 90.0f, 50.0f, 70.0f, -90.0f, 90.0f)));
#endif
				rigidBody.UpdateTransform(ref position, ref orientation);

				rigidBody.Rotation = random.NextVector3(-0.5f, 0.5f);

				// Assign the sphere to the quad-tree and shape list.
				this.spheres.Add(sphere);
				this.physics.RigidBodies.Add(rigidBody);
			}
		}

		#region ICameraCallback Members

		public void GetTransform(ref Vector3 position, ref Matrix orientation)
		{
			if (this.spheres.Count > 0)
			{
				position = this.spheres[0].Position;
			}
		}

		#endregion
	}
}


